/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package network;

import core.IMAPMIDlet;
import core.NotificationManager;
import core.RuntimeData;
import core.Settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Timer;
import java.util.TimerTask;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.io.Connector;
import javax.microedition.io.SocketConnection;
import network.packets.Packet;
import utils.Utils;

/**
 *
 * @author Kvest
 */
public class Connection implements Runnable
{
//    public static String LOG = "";
    
    private static final long DEFAULT_PING_TIME = 60000L;
    private static final long RECONNECT_PERIOD = 15000L;
    private static final long MAX_RECONNECT_COUNT = 5;
    
    private static Connection connection = null;
    private static final Object lock = new Object();
    
    public static Connection getConnection()
    {
        synchronized(lock)
        {
            if (connection == null)
            {
                connection = new Connection();
            }
        }
        
        return connection;
    }
    
    private SocketConnection sc;
    private InputStream is;
    private OutputStream os;
    private String host;
    private volatile boolean z;//флаг основного цыкла
    
    private PacketParser packetParser;
    
    //Слушатели
    private ConnectionErrorListener connectionErrorListener = null;
    private ConnectionNotifier connectionNotifier = null;
    private Hashtable packetNotifiers;
    
    //Для пинговки
    private long pingTime;
    private Timer pingTimer;
    private boolean sended;
    private byte[] pingData;

    private Timer reconnectTimer = null;
    private int reconnectsCount = 0;
    private static final Object reconnectTimerLock = new Object();

    private Connection()
    {   
        sc = null;
        is = null;
        os = null;
        z = false;
        
        pingTime = DEFAULT_PING_TIME;
        
        packetNotifiers = new Hashtable();
        packetParser = new PacketParser();
    }
    
    public void start(String host)//Функция запуска потока
    {
        if (isConnected())
        {
            stop();
        }
        
        this.host = host;
        Thread thread = new Thread(this);
        z = true;
        thread.start();
    }
    
    public void stop()//Функция остоновки потока
    {
        z = false;
        disconnect();
    }
    
    
    public void run()
    {
        while(z)
        {
            if (sc == null)
            {
                try
                {
                    sc = (SocketConnection)Connector.open("socket://" + host, Connector.READ_WRITE, true);
//                    try {
//                        LOG += ("RCVBUF=" + sc.getSocketOption(SocketConnection.RCVBUF) + "\n");
//                    } catch(Exception e) {}
                    try
                    {
                        sc.setSocketOption(SocketConnection.KEEPALIVE, 1);
                    }catch(Exception ex){}
                    is = sc.openInputStream();
                    os = sc.openOutputStream();

                    if (connectionNotifier != null)
                    {
                        connectionNotifier.connectionEstablished();
                    }
                    if (!z)
                    {
                        return;
                    }
                    
                    startPingTimer();
                }
                catch(ConnectionNotFoundException cnfe)
                {
                    disconnect();
                    notifyConnectionErrorListener(ConnectionErrorListener.CONNECTION_NOT_FOUND);
//                    addErr(1, cnfe.getMessage());
                    return;
                }
                catch(IOException ioe)
                {
                    disconnect();
                    notifyConnectionErrorListener(ConnectionErrorListener.IO_EXCEPTION);
//                    addErr(2, ioe.getMessage());
                    return;
                }
                catch(SecurityException se)
                {
                    disconnect();
                    notifyConnectionErrorListener(ConnectionErrorListener.SECURITY_EXCEPTION);
//                    addErr(3, se.getMessage());
                    return;
                }
                catch(Exception e)
                {
                    disconnect();
                    notifyConnectionErrorListener(ConnectionErrorListener.EXCEPTION);
//                    addErr(4, e.getMessage());
                    return;
                }
            }
//int step = 0;
            //Читаем данные
            try
            {
                //Длинна пакета
                byte[] buff;
                buff = new byte[4];
                for (int i = 0; i < 4; i++)
                {
                    buff[i] = (byte)is.read();                    
                }
//step = 1;
                //Версия протокола(пока не используем)
                for (int i = 0; i < 4; i++)
                {
                    is.read();
                }
//step = 2;
                //Определяем размер
                int size = Utils.byteToInt(buff);
                buff = new byte[size];               
//step = 3;                
                //Читаем данные
                int read = is.read(buff, 0, size);
//step = 4;
                int reread = read;
                while (read < size /*&& is.available() > 0*/ && reread != -1)   
                {
                    reread = is.read(buff,read, size - read);
                    read += reread;
                }
//step = 5;
                if (read == size)
                {
                    //Парсим пакет
                    Packet packet = parsePacket(buff);
// step = 6;                   
                    //Отдаем на обработку
                    packetNotify(packet);
                }
                else
                {                    
                    disconnect();
                    notifyConnectionErrorListener(ConnectionErrorListener.WRONG_PACKET_SIZE);
//                    addErr(5, "-");
                    return;
                }
            }
            catch(IOException ioe)
            {       
                disconnect();
                notifyConnectionErrorListener(ConnectionErrorListener.IO_EXCEPTION);
//                addErr(6, ioe.getMessage() + ":" + step);
                return;
            }
            catch(Exception e)
            {         
                disconnect();
                notifyConnectionErrorListener(ConnectionErrorListener.EXCEPTION);
//                addErr(7, e.getMessage());
                return;
            }           
            Thread.yield();
        }
    }
    
    private Packet parsePacket(byte[] data)
    {
        return packetParser.parsePacket(data);
    }
    
    private void packetNotify(Packet packet)
    {
        if (packet == null)
        {
            return;
        }
        
        Integer packetId = new Integer(packet.getId());
        
        if (packetNotifiers.containsKey(packetId))
        {
            ((PacketNotifier)packetNotifiers.get(packetId)).incomingPacket(packet);
        }
    }
    
    private void startPingTimer()
    {
        pingData = null;
        stopPingTimer();
        
        sended = false;
        pingTimer = new Timer();
        pingTimer.schedule(new TimerTask()
        {
            public void run()
            {
                if (!sended)
                {
                    if (pingData == null)
                    {
                        RuntimeData runtimeData = IMAPMIDlet.getMidlet().getRuntimeData();
                        pingData = PacketBuilder.createSrvTransfereData(PacketBuilder.DEFAULT_CONNECTION_TYPE, runtimeData.getSrvID(), 
                                                           PacketBuilder.DEFAULT_DESTINATION_ID, runtimeData.getGUID(), true, 
                                                           PacketBuilder.createBodyUpdateJavaConnectionState(runtimeData.getPeopleID()));
                    }                 
                    send(pingData);
                }
                sended = false;
            }
        }, pingTime, pingTime);
    }
    
    private void stopPingTimer()
    {
        if (pingTimer != null)
        {
            pingTimer.cancel();
        }

        pingTimer = null;
    }
    
    public synchronized boolean send(byte[] buff)//Функция отправки данных по сети
    {
        try
        {
            if (os != null)//Если исходяший поток созданн,то отправляем данные
            {
                os.write(buff);
                os.flush();
                sended = true;
                return true;
            }
            else//Если не создан - возвращаем false
            {
                return false;
            }
        }
        catch(IOException ioe)
        {
            return false;
        }
    }
    
    public void disconnect()//Функция разрыва соединения
    {
        stopPingTimer();
        
        try
        {
            if (os != null)
            {
                os.close();
            }
            os = null;
        }catch(Exception e){}
        try
        {
            if (is != null)
            {
                is.close();
            }
            is = null;
        }catch(Exception e){}
        try
        {
            if (sc != null)
            {
                sc.close();
            }
            sc = null;
        }catch(Exception e){}
    }
    
    public boolean isConnected()
    {
        return (sc != null);
    }
    
    public void setConnectionErrorListener(ConnectionErrorListener listener)
    {
        connectionErrorListener = listener;
    }
    
    private void notifyConnectionErrorListener(int code)
    {
        if (z)
        {
            NotificationManager.getNotificationManager().event(NotificationManager.CONNECTION_ERROR);
        }
        if (connectionErrorListener != null && z)
        {
            connectionErrorListener.connectionError(code);
        }
    }
    
    public void setPacketListener(int id, PacketNotifier listener)
    {
        packetNotifiers.put(new Integer(id), listener);
    }
    
    public void deletePacketListener(int id)
    {
        packetNotifiers.remove(new Integer(id));
    }
    
    public void setConnectionNotifier(ConnectionNotifier listener)
    {
        connectionNotifier = listener;
    }

    public void setPingTime(long pingTime) 
    {
        long oldPingTime = this.pingTime;
        this.pingTime = pingTime;
        
        //Если поменялось время пинга - перезапускаем таймер
        if (oldPingTime != this.pingTime)
        {
            startPingTimer();
        }
    }

    public void startReconnectTimer()
    {
//System.out.println("startReconnectTimer(" + reconnectsCount + ")");        
        synchronized(reconnectTimerLock)
        {
            //Если таймер запущен - останавливаем его
            if (reconnectTimer != null)
            {
                try
                {
                    reconnectTimer.cancel();
                }catch(Exception ex){}
            }
            
            //проверяем, нужно ли еще выполнять автоподключение
            if (reconnectsCount == MAX_RECONNECT_COUNT) {
//   System.out.println("reconnection count reached");             
                ++reconnectsCount;
                if (connectionNotifier != null) {
                    connectionNotifier.onReconnectFailed();
                }
                return;
            } else if (reconnectsCount == (2 * MAX_RECONNECT_COUNT + 1)) {
//  System.out.println("Extra reconnection count reached");   
                if (connectionNotifier != null) {
                    connectionNotifier.onReconnectToExtraFailed();
                }
                return;
            }
                   
            //Запускаем переподключение
            reconnectTimer = new Timer();
            reconnectTimer.schedule(new TimerTask()
            {
                public void run()
                {
                    synchronized(reconnectTimerLock) 
                    {
                        //Проверяем - нет ли уже соединения
                        if (isConnected())
                        {
                            return;
                        }
//  System.out.println("Reconnect(" + reconnectsCount + ")");                        
                        //Начинаем соединение
                        if (reconnectsCount < MAX_RECONNECT_COUNT) {
//   System.out.println("Reconnect main");                           
                            start(Settings.getSettings().getServerIP());
                        } else {
//   System.out.println("ExtraReconnect main");                           
                            start(Settings.getSettings().getExtraServerIP());
                        }
                        
                        //Инкрементим счетчик
                        ++reconnectsCount;
                    }
                }
            }, RECONNECT_PERIOD);
        }
    }

    public void stopReconnectTimer()
    {
        synchronized(reconnectTimerLock)
        {
            if (reconnectTimer != null)
            {
                try
                {
                    reconnectTimer.cancel();
                }catch(Exception ex){}
            }
            reconnectTimer = null;
            reconnectsCount = 0;
        }
    }

    public interface ConnectionErrorListener
    {
        public static final int CONNECTION_NOT_FOUND = 1;
        public static final int IO_EXCEPTION = 2;
        public static final int SECURITY_EXCEPTION = 3;
        public static final int EXCEPTION = 4;
        public static final int WRONG_PACKET_SIZE = 5;
        
        public void connectionError(int code);
    }
    
    public interface ConnectionNotifier
    {
        public void connectionEstablished();
        public void onReconnectFailed();
        public void onReconnectToExtraFailed();
    }
    
    public interface PacketNotifier
    {
        public void incomingPacket(Packet packet);
    }
    
//    private void addErr(int id, String message) {
//        LOG += ("err[" + id + "," + message + "]");
//    }
}
